"use client"
import * as React from 'react'
import { ChevronRight, Folder, FileText, FileJson, FileCode2, File as FileIcon, Lock, GitBranch, Coins, Copy } from 'lucide-react'
import { Button } from '@open-mercato/ui/primitives/button'
import { Input } from '@open-mercato/ui/primitives/input'
import { Tag } from '@open-mercato/ui/primitives/tag'
import { Spinner } from '@open-mercato/ui/primitives/spinner'
import { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
import { MarkdownContent } from '@open-mercato/ui/backend/markdown/MarkdownContent'
import { flash } from '@open-mercato/ui/backend/FlashMessages'
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
import { formatNumber, type AgentDetailView } from '../../../../components/types'
import {
buildFileTree,
filterTree,
fileKind,
FILE_KIND_LABEL,
formatBytes,
splitFrontmatter,
utf8Bytes,
type AgentFilesResponse,
type AgentSourceFile,
type FileTreeNode,
} from './filesShared'
type FilesTabProps = {
agentId: string
agent: AgentDetailView
active: boolean
}
function FileGlyph({ name }: { name: string }) {
const kind = fileKind(name)
const className = 'size-4 shrink-0 text-muted-foreground'
if (kind === 'md') return
if (kind === 'json') return
if (kind === 'ts') return
return
}
function CodeView({ content }: { content: string }) {
const body = content.replace(/\n$/, '')
const lineCount = body.split('\n').length
const gutter = Array.from({ length: lineCount }, (_, i) => i + 1).join('\n')
return (
)
}
function FileReader({ file }: { file: AgentSourceFile }) {
const t = useT()
const locale = useLocale()
const [view, setView] = React.useState<'preview' | 'raw'>('preview')
React.useEffect(() => { setView('preview') }, [file.path])
const kind = fileKind(file.path.split('/').pop() as string)
const segments = file.path.split('/')
const leaf = segments[segments.length - 1]
const lineCount = file.content.replace(/\n$/, '').split('\n').length
const meta = file.inContext
? `${FILE_KIND_LABEL[kind]} · ${lineCount} ${t('agent_orchestrator.files.lines', 'lines')} · ${formatBytes(utf8Bytes(file.content))} · ~${formatNumber(file.tokens, locale)} ${t('agent_orchestrator.files.tokens', 'tokens')}`
: `${FILE_KIND_LABEL[kind]} · ${lineCount} ${t('agent_orchestrator.files.lines', 'lines')} · ${formatBytes(utf8Bytes(file.content))} · ${t('agent_orchestrator.files.notInContext', 'not in model context')}`
const copy = () => {
if (typeof navigator === 'undefined' || !navigator.clipboard) return
navigator.clipboard.writeText(file.content).then(
() => flash(t('agent_orchestrator.files.copied', 'File copied to clipboard.'), 'success'),
() => flash(t('agent_orchestrator.files.copyFailed', 'Could not copy to clipboard.'), 'error'),
)
}
const { frontmatter, body } = kind === 'md' ? splitFrontmatter(file.content) : { frontmatter: null, body: file.content }
return (
{agentRoot(segments)}
{leaf}
{meta}
{kind === 'md' ? (
setView(value as 'preview' | 'raw')} aria-label={t('agent_orchestrator.files.viewToggle', 'File view')}>
{t('agent_orchestrator.files.preview', 'Preview')}
{t('agent_orchestrator.files.raw', 'Raw')}
) : null}
{kind === 'md' && view === 'preview' ? (
{frontmatter && frontmatter.length ? (
{frontmatter.map((row) => (
- {row.key}
- {row.value}
))}
) : null}
) : (
)}
)
}
function agentRoot(segments: string[]): string {
if (segments.length <= 1) return ''
return `${segments.slice(0, -1).join('/')}/`
}
function TreeNode({
node,
depth,
selectedPath,
onSelect,
locale,
}: {
node: FileTreeNode
depth: number
selectedPath: string | null
onSelect: (path: string) => void
locale: string
}) {
const t = useT()
const [open, setOpen] = React.useState(true)
const pad = { paddingLeft: `${6 + depth * 16}px` }
if (node.type === 'file') {
const selected = selectedPath === node.path
return (
)
}
return (
{open ? (
{(node.children ?? []).map((child) => (
))}
) : null}
)
}
function DefinitionOverview({ agent }: { agent: AgentDetailView }) {
const t = useT()
const locale = useLocale()
const defaultValue = t('agent_orchestrator.agentDetail.defaultValue', 'Default')
const items: Array<{ key: string; value: string; mono?: boolean }> = [
{ key: t('agent_orchestrator.agentDetail.config.runtimeKind', 'Runtime'), value: `${agent.runtime} · ${t('agent_orchestrator.files.fileDefined', 'file-defined')}`, mono: true },
{ key: t('agent_orchestrator.agentDetail.fields.provider', 'Provider'), value: agent.defaultProvider ?? defaultValue, mono: true },
{ key: t('agent_orchestrator.agentDetail.fields.model', 'Model'), value: agent.defaultModel ?? defaultValue, mono: true },
{ key: t('agent_orchestrator.agentDetail.fields.maxSteps', 'Max steps'), value: agent.loopMaxSteps != null ? String(agent.loopMaxSteps) : defaultValue },
{ key: t('agent_orchestrator.agentDetail.config.resultKind', 'Result kind'), value: agent.resultKind },
]
if (agent.tokenUsage) {
items.push({ key: t('agent_orchestrator.files.contextEstimate', 'Context estimate'), value: `~${formatNumber(agent.tokenUsage.total, locale)} ${t('agent_orchestrator.files.tokens', 'tokens')}` })
}
return (
{items.map((item) => (
{item.key}
{item.value}
))}
{agent.tools.length ?
: null}
{agent.skills.length ?
: null}
{agent.subAgents.length ?
: null}
)
}
function ChipGroup({ label, values }: { label: string; values: string[] }) {
return (
{label}
{values.map((value) => {value})}
)
}
export function FilesTab({ agentId, agent, active }: FilesTabProps) {
const t = useT()
const locale = useLocale()
const [files, setFiles] = React.useState(null)
const [state, setState] = React.useState<'idle' | 'loading' | 'ready' | 'error'>('idle')
const [selectedPath, setSelectedPath] = React.useState(null)
const [filter, setFilter] = React.useState('')
// Dedupe by agent id (survives React's dev double-invoke) instead of a
// cleanup-cancel guard, which would drop the sole in-flight fetch on remount.
const loadedForRef = React.useRef(null)
React.useEffect(() => {
if (!active || loadedForRef.current === agentId) return
loadedForRef.current = agentId
setState('loading')
apiCall(`/api/agent_orchestrator/agents/${encodeURIComponent(agentId)}/files`)
.then((call) => {
if (loadedForRef.current !== agentId) return
if (!call.ok || !call.result) {
setState('error')
return
}
const items = call.result.files ?? []
setFiles(items)
const preferred = items.find((f) => f.path === 'AGENT.md') ?? items[0]
setSelectedPath(preferred ? preferred.path : null)
setState('ready')
})
.catch(() => { if (loadedForRef.current === agentId) setState('error') })
}, [active, agentId])
const tree = React.useMemo(() => (files ? buildFileTree(files) : []), [files])
const visibleTree = React.useMemo(() => filterTree(tree, filter), [tree, filter])
const selectedFile = React.useMemo(
() => (files && selectedPath ? files.find((f) => f.path === selectedPath) ?? null : null),
[files, selectedPath],
)
const contextTotal = agent.tokenUsage?.total ?? (files ? files.filter((f) => f.inContext).reduce((sum, f) => sum + f.tokens, 0) : 0)
return (
{agent.id.includes('.') ? agent.id.split('.').slice(-1)[0] : agent.id}
{t('agent_orchestrator.files.definition', 'definition')}
{files ?
{t('agent_orchestrator.files.fileCount', undefined, { count: files.length })} : null}
~{formatNumber(contextTotal, locale)} {t('agent_orchestrator.files.tokens', 'tokens')}
{t('agent_orchestrator.files.readOnly', 'Read-only')}
{state === 'loading' || state === 'idle' ? (
{t('agent_orchestrator.files.loading', 'Loading files…')}
) : state === 'error' ? (
) : (
setFilter(e.target.value)} placeholder={t('agent_orchestrator.files.filter', 'Filter files…')} className="h-8" />
{t('agent_orchestrator.files.tokenLegend', 'Numbers are the o200k_base token estimate each file adds to context; sample inputs are not counted.')}
{visibleTree.length ? (
visibleTree.map((node) => (
))
) : (
{t('agent_orchestrator.files.noMatches', 'No files match your filter.')}
)}
{selectedFile ?
: (
{t('agent_orchestrator.files.selectPrompt', 'Select a file to read it.')}
)}
)}
)
}
export default FilesTab